That's not how push ax works. Your code example of what it is "equal to" should be:
sub sp, 2
mov [ss:sp], ax
You don't overwrite the value of SP with AX. You instead copy AX into the memory address pointed to by SS:SP (using the stack segment, rather than the data segment)... But actually, this isn't even accurate. What you really need is something like this:
mov [tmp], sp
pushf ;push FLAGS
sub [tmp], 2
popf
mov sp, [tmp]
mov [ss:sp], ax
Basically, push does something quite simple, but the details around that simple thing make it very worth making it's own instruction. Especially being able to copy memory-to-memory with instructions like push word [bp - 4] to push a local variable if you didn't already have it loaded.
Alternate code that doesn't need, and doesn't use the imaginary [ss:sp] addressing mode that's not encodeable as a 16-bit addressing mode.
mov internal_tmp, sp
lea internal_tmp, [internal_tmp - 2] ; sub without FLAGS
mov [internal_tmp], SRC_OPERAND ; even for a memory source, or for push sp
mov sp, internal_tmp