Intuition
You will write the four sweeps, test it on a square matrix, watch it pass, and ship it. Then a 3x4 or a single-row matrix comes along and a whole row appears twice in your output. The reason is that you only check top <= bottom and left <= right at the top of the while loop, but the boundaries move three times inside one iteration. On a thin remainder the top sweep consumes the last row and pushes top past bottom, and the bottom sweep then re-reads that exact same row because nobody re-checked. The fix is not clever, it is disciplined: re-test top <= bottom immediately before the bottom sweep, and left <= right immediately before the left sweep. The invariant you are protecting is that the rectangle bounded by the four walls always contains exactly the cells not yet emitted, so a sweep is only legal while that rectangle is still non-empty.
Approach
Four walls, not four directions
Rather than tracking a heading and turning, keep four integers: top, bottom, left and right. They fence off the sub-rectangle that has not been read yet. Each of the four sweeps reads one full wall of that rectangle and then retracts its own boundary inward by one. Thinking in walls rather than turns is what makes the termination condition expressible at all: the spiral is finished exactly when the rectangle becomes empty.
Sweep, then retract, in a fixed order
Go left-to-right along the top row and increment top; top-to-bottom along the right column and decrement right; right-to-left along the bottom row and decrement bottom; bottom-to-top along the left column and increment left. Emitting before retracting matters, because the retraction is what marks those cells as consumed. Doing it in the other order would skip a wall on the first pass.
Re-check the walls mid-loop
This is the part everyone gets wrong. After the first two sweeps, top and right have already moved, so the loop condition tested at the top of the iteration is stale. Guard the bottom sweep with top <= bottom and the left sweep with left <= right. Without them, a matrix with a single remaining row or column emits that row or column twice, because the return sweep walks back over ground the outbound sweep already covered.
Solution & live demo
Edge cases
Return an empty list before entering the loop; matrix[0] would otherwise raise.
The top sweep takes everything and pushes top past bottom; the mid-loop guard skips the bottom sweep so nothing is duplicated.
Top then right sweeps take everything and right falls below left; the left-sweep guard prevents a second pass.
Rows and columns exhaust at different times, so the two guards fire independently rather than together.